﻿namespace Example.Cryptography
{
    using System;
    using System.Diagnostics.CodeAnalysis;
    using System.Runtime.CompilerServices;
    using System.Runtime.InteropServices;
    using System.Security;
    using System.Text;

    public struct SensitiveData : IComparable
    {
        private readonly SecureString _data;

        public SensitiveData(string data)
            : this(ToSecureString(data))
        {
        }

        public SensitiveData(SecureString data)
        {
            _data = data;
        }

        public static bool operator ==(SensitiveData operand1, 
                                       SensitiveData operand2)
        {
            return 0 == Compare(operand1, operand2);
        }

        public static bool operator >(SensitiveData operand1, 
                                      SensitiveData operand2)
        {
            return Compare(operand1, operand2) > 0;
        }

        public static bool operator !=(SensitiveData operand1, 
                                       SensitiveData operand2)
        {
            return 0 != Compare(operand1, operand2);
        }

        public static bool operator <(SensitiveData operand1, 
                                      SensitiveData operand2)
        {
            return Compare(operand1, operand2) < 0;
        }

        public static int Compare(SensitiveData comparand1, 
                                  SensitiveData comparand2)
        {
            var left = comparand1.ToInsecureBytes();
            var right = comparand2.ToInsecureBytes();

            int compare;

            if (null == left)
            {
                compare = (null == right) ? 0 : -1;
            }
            else if (null == right)
            {
                compare = 1;
            }
            else
            {
                compare = (left.Length == right.Length) ? 0 : (left.Length < right.Length) ? -1 : 1;

                for (var i = 0; i < left.Length; i++)
                {
                    if (left[i] >
                        right[i])
                    {
                        compare = 1;
                    }
                    else if (left[i] <
                             right[i])
                    {
                        compare = -1;
                    }
                    else if (i == (right.Length - 1))
                    {
                        break;
                    }
                }

                if (left != null)
                {
                    for (var i = 0; i < left.Length; i++)
                    {
                        left[i] = 0;
                    }
                }

                if (right != null)
                {
                    for (var i = 0; i < right.Length; i++)
                    {
                        right[i] = 0;
                    }
                }
            }

            return compare;
        }

        public override bool Equals(object obj)
        {
            var equals = false;

            if (null != obj)
            {
                if (obj is SensitiveData)
                {
                    equals = 0 == CompareTo(obj);
                }
            }

            return equals;
        }

        public override int GetHashCode()
        {
            return (null == _data) ? 0 : _data.GetHashCode();
        }

        public byte[] ToInsecureBytes()
        {
            byte[] bytes = null;

            if (null != _data)
            {
                GCHandle? gc = null;
                var handle = IntPtr.Zero;
                var length = _data.Length;

                RuntimeHelpers.PrepareConstrainedRegions();
                try
                {
                    handle = Marshal.SecureStringToGlobalAllocAnsi(_data);
                    bytes = new byte[length];
                    gc = GCHandle.Alloc(bytes, GCHandleType.Pinned);
                    for (var i = 0; i < length; i++)
                    {
                        bytes[i] = Marshal.ReadByte(handle, i);
                    }
                }
                finally
                {
                    if (handle != IntPtr.Zero)
                    {
                        Marshal.ZeroFreeGlobalAllocAnsi(handle);
                    }

                    if (null != gc &&
                        gc.HasValue)
                    {
                        gc.Value.Free();
                    }
                }
            }

            return bytes;
        }

        public char[] ToInsecureChars()
        {
            char[] value = null;

            var bytes = ToInsecureBytes();
            if (bytes != null)
            {
                value = new char[bytes.Length];
                for (var i = 0; i < bytes.Length; i++)
                {
                    value[i] = Convert.ToChar(bytes[i]);
                }
            }

            return value;
        }

        public string ToInsecureString()
        {
            StringBuilder buffer = null;
            var bytes = ToInsecureBytes();
            if (bytes != null)
            {
                buffer = new StringBuilder(bytes.Length);
                foreach (var value in bytes)
                {
                    buffer.Append(Convert.ToChar(value));
                }
            }

            return null == buffer ? null : buffer.ToString();
        }

        public SecureString ToSecureString()
        {
            return _data;
        }

        public override string ToString()
        {
            return string.Empty;
        }

        public int CompareTo(object obj)
        {
            int compare;

            if (null == obj)
            {
                compare = 1;
            }
            else
            {
                if (obj is SensitiveData)
                {
                    compare = Compare(this, (SensitiveData)obj);
                }
                else
                {
                    throw new ArgumentException("The object must be of type \"SensitiveData\".");
                }
            }

            return compare;
        }

        [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "It is the responsibility of the calling code to dispose.")]
        private static SecureString ToSecureString(string value)
        {
            SecureString secret = null;

            if (value != null)
            {
                secret = new SecureString();
                foreach (var character in value)
                {
                    secret.AppendChar(character);
                }
            }

            return secret;
        }
    }
}